package com.luych.toolbox.base.service.service;

import com.luych.toolbox.base.service.constant.Constant;
import com.luych.toolbox.base.service.dao.*;
import com.luych.toolbox.base.service.entity.*;
import com.luych.toolbox.common.constant.SpringCacheConstant;
import com.luych.toolbox.common.feign.intf.gateway.center.FileFeign;
import com.luych.toolbox.common.feign.intf.gateway.center.WebSocketFeign;
import com.luych.toolbox.common.feign.param.base.service.UserParam;
import com.luych.toolbox.common.feign.view.base.service.PermissionView;
import com.luych.toolbox.common.feign.view.base.service.RoleView;
import com.luych.toolbox.common.feign.view.base.service.UserView;
import com.luych.toolbox.common.feign.view.gateway.center.WebSocketSession;
import com.luych.toolbox.common.page.PageParam;
import com.luych.toolbox.common.page.PageResult;
import com.luych.toolbox.common.service.base.BaseConvert;
import com.luych.toolbox.common.service.base.BaseService;
import com.luych.toolbox.common.service.exception.HttpException;
import com.luych.toolbox.common.service.page.SpringPageParamAdapter;
import com.luych.toolbox.common.service.page.SpringPageResultAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
public class UserService extends BaseService<UserView, UserParam, User> {

    private static final Logger logger = LoggerFactory.getLogger(UserService.class);

    private static final BaseConvert<UserParam, User> userParamToEntity = User::new;
    private static final BaseConvert<User, UserView> userEntityToView = UserView::new;
    private static final BaseConvert<Role, RoleView> roleEntityToView = RoleView::new;
    private static final BaseConvert<Permission, PermissionView> permissionEntityToView = PermissionView::new;

    private static final String ADMIN = "adminId";
    private final WebSocketFeign webSocketFeign;
    private final FileFeign fileFeign;
    private final PasswordEncoder passwordEncoder;
    private final UserDao userDao;
    private final RoleDao roleDao;
    private final PermissionDao permissionDao;
    private final UserRRoleDao userRRoleDao;
    private final RoleRPermissionDao roleRPermissionDao;

    @Autowired
    public UserService(WebSocketFeign webSocketFeign,
                       FileFeign fileFeign,
                       PasswordEncoder passwordEncoder,
                       UserDao userDao,
                       RoleDao roleDao,
                       PermissionDao permissionDao,
                       UserRRoleDao userRRoleDao,
                       RoleRPermissionDao roleRPermissionDao) {
        super(userDao, userParamToEntity, userEntityToView);
        this.webSocketFeign = webSocketFeign;
        this.fileFeign = fileFeign;
        this.passwordEncoder = passwordEncoder;
        this.userDao = userDao;
        this.roleDao = roleDao;
        this.permissionDao = permissionDao;
        this.userRRoleDao = userRRoleDao;
        this.roleRPermissionDao = roleRPermissionDao;
    }

    @Override
    protected UserView fill(UserView view) {
        // get roles
        List<Role> roles = new ArrayList<>();
        List<UserRRole> userRRoles = userRRoleDao.findByUserId(view.getId());
        for (UserRRole userRRole : userRRoles) {
            Optional<Role> role = roleDao.findById(userRRole.getRoleId());
            role.ifPresent(roles::add);
        }
        view.setRoles(roles.stream().map(roleEntityToView).collect(Collectors.toList()));
        // get permissions
        List<Permission> permissions = new ArrayList<>();
        for (Role role : roles) {
            List<RoleRPermission> roleRPermissions = roleRPermissionDao.findByRoleId(role.getId());
            for (RoleRPermission roleRPermission : roleRPermissions) {
                Optional<Permission> permission = permissionDao.findById(roleRPermission.getPermissionId());
                permission.ifPresent(permissions::add);
            }
        }
        view.setPermissions(permissions.stream().map(permissionEntityToView).collect(Collectors.toList()));
        // if online
        List<WebSocketSession> sessions = webSocketFeign.get();
        boolean online = sessions.stream().map(WebSocketSession::getUserId).anyMatch(view.getId()::equals);
        view.setOnline(online);
        view.setOnlineStatus(online ? Constant.USER_ONLINE : Constant.USER_OFFLINE);
        // return
        return view;
    }

    @Override
    @Cacheable(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_ID, key = "#id")
    public Optional<UserView> get(String id) {
        return super.get(id);
    }

    @Override
    @Transactional
    @Caching(
            cacheable = {
                    @Cacheable(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_ID, key = "#param.id"),
                    @Cacheable(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_LOGIN_NAME, key = "#param.loginName")
            }
    )
    public UserView create(UserParam param) {
        Assert.notNull(param.getUserName(), "Param userName can't be empty");
        Assert.notNull(param.getLoginName(), "Param loginName can't be empty");
        Assert.notNull(param.getPassWord(), "Param passWord can't be empty");
        Assert.notEmpty(param.getRoleIds(), "Param roleIds can't be empty");
        // check name exists
        if (userDao.findByLoginName(param.getLoginName()).isPresent()) {
            throw new HttpException(HttpStatus.CONFLICT, Constant.USER_SAVE_NAME_USED);
        }
        // check role exists
        List<String> roleIds = roleDao.findAll().stream().map(Role::getId).collect(Collectors.toList());
        List<String> notExistRoleIds = param.getRoleIds().stream().filter(paramRoleId -> !roleIds.contains(paramRoleId)).collect(Collectors.toList());
        if (!notExistRoleIds.isEmpty()) {
            throw new RuntimeException("Can't find role with id: " + String.join(",", notExistRoleIds));
        }
        // set password
        param.setPassWord(passwordEncoder.encode(param.getPassWord()));
        // save user
        User user = userDao.save(userParamToEntity.apply(param));
        // save user role
        for (String roleId : param.getRoleIds()) {
            UserRRole userRRole = new UserRRole();
            userRRole.setId(UUID.randomUUID().toString());
            userRRole.setRoleId(roleId);
            userRRole.setUserId(user.getId());
            userRRoleDao.save(userRRole);
        }
        // confitm image
        if (StringUtils.hasText(param.getImage())) {
            fileFeign.confirm(param.getImage());
        }
        // final, return
        UserView userView = userEntityToView.apply(user);
        this.fill(userView);
        return userView;
    }

    @Override
    @Transactional
    @Caching(
            put = {
                    @CachePut(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_ID, key = "#param.id"),
                    @CachePut(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_LOGIN_NAME, key = "#param.loginName")
            }
    )
    public UserView modify(UserParam param) {
        Assert.notNull(param.getId(), "Param id can't be empty");
        Assert.notNull(param.getUserName(), "Param userName can't be empty");
        Assert.notNull(param.getLoginName(), "Param loginName can't be empty");

        User oldUser = userDao.findById(param.getId()).orElseThrow(() ->
                new HttpException(HttpStatus.NOT_FOUND, "Can't find User with id: " + param.getId()));
        // check name exists
        Optional<User> userOptOfName = userDao.findByLoginName(param.getLoginName());
        if (userOptOfName.isPresent() && !userOptOfName.get().getId().equals(param.getId())) {
            throw new HttpException(HttpStatus.CONFLICT, Constant.USER_SAVE_NAME_USED);
        }
        // check role exists if set
        if (param.getRoleIds() != null) {
            List<String> allRoleIds = roleDao.findAll().stream().map(Role::getId).collect(Collectors.toList());
            List<String> notExistRoleIds = param.getRoleIds().stream().filter(paramRoleId -> !allRoleIds.contains(paramRoleId)).collect(Collectors.toList());
            if (!notExistRoleIds.isEmpty()) {
                throw new RuntimeException("Can't find role with id: " + String.join(",", notExistRoleIds));
            }
        }
        // set password
        if (StringUtils.isEmpty(param.getPassWord())) {
            param.setPassWord(oldUser.getPassWord());
        } else if (!oldUser.getPassWord().equals(param.getPassWord())) {
            param.setPassWord(passwordEncoder.encode(param.getPassWord()));
        }
        // save user
        User newUser = userDao.save(userParamToEntity.apply(param));
        // if set roleIds in param, then we update roles of user here
        if (param.getRoleIds() != null) {
            List<String> oldRoleIds = userRRoleDao.findByUserId(newUser.getId()).stream()
                    .map(UserRRole::getRoleId).collect(Collectors.toList());
            List<String> newRoleIds = param.getRoleIds();
            oldRoleIds.stream().filter(oldRoleId -> !newRoleIds.contains(oldRoleId))
                    .forEach(oldRoleId -> userRRoleDao.deleteByUserIdAndRoleId(newUser.getId(), oldRoleId));
            newRoleIds.stream().filter(newRoleId -> !oldRoleIds.contains(newRoleId))
                    .forEach(newRoleId -> {
                        UserRRole userRRole = new UserRRole();
                        userRRole.setId(UUID.randomUUID().toString());
                        userRRole.setRoleId(newRoleId);
                        userRRole.setUserId(newUser.getId());
                        userRRoleDao.save(userRRole);
                    });
        }
        // confitm image
        if (StringUtils.hasText(param.getImage())) {
            fileFeign.confirm(param.getImage());
        }
        // final, return
        UserView view = userEntityToView.apply(newUser);
        this.fill(view);
        return view;
    }

    @Override
    @Transactional
    @CacheEvict(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_ID, key = "#id")
    public void remove(String id) {
        if (ADMIN.equals(id)) {
            throw new HttpException(HttpStatus.LOCKED, Constant.USER_DELETE_REJECT_ADMIN);
        }

        Optional<User> user = userDao.findById(id);
        if (!user.isPresent()) {
            throw new HttpException(HttpStatus.NOT_FOUND);
        }

        userRRoleDao.deleteByUserId(id);
        super.remove(id);

        fileFeign.delete(user.get().getImage());
    }

    @CacheEvict(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_ID, key = "#id")
    public void removeCacheById(String id) {

    }

    @CacheEvict(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_LOGIN_NAME, key = "#loginName")
    public void removeCacheByLoginName(String loginName) {

    }

    @Transactional
    @Caching(
            evict = {
                    @CacheEvict(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_ID, key = "#userParam.id"),
                    @CacheEvict(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_LOGIN_NAME, key = "#userParam.loginName")
            }
    )
    public void remove(UserParam userParam) {
        this.remove(userParam.getId());
    }

    public void addRole(String userId, String roleId) {
        UserRRole userRRole = new UserRRole();
        userRRole.setId(UUID.randomUUID().toString());
        userRRole.setUserId(userId);
        userRRole.setRoleId(roleId);
        userRRoleDao.save(userRRole);
    }

    public void removeRole(String userId, String roleId) {
        Optional<UserRRole> userRRole = userRRoleDao.getByUserIdAndRoleId(userId, roleId);
        userRRole.ifPresent(userRRoleDao::delete);
    }

    public List<RoleView> listRole(String userId) {
        List<UserRRole> userRRoles = userRRoleDao.findByUserId(userId);
        return userRRoles.stream().map(UserRRole::getRoleId).map(roleDao::findById).filter(Optional::isPresent)
                .map(Optional::get).map(roleEntityToView).collect(Collectors.toList());
    }

    @Cacheable(cacheNames = SpringCacheConstant.USER_PROVIDER__USER__BY_LOGIN_NAME, key = "#loginName")
    public Optional<UserView> getByLoginName(String loginName) {
        Optional<User> user = userDao.findByLoginName(loginName);
        return user.map(userEntityToView).map(this::fill);
    }

    public PageResult<UserView> pageByKeyword(PageParam pageParam, String keyword) {
        Page<User> page;
        if (StringUtils.isEmpty(keyword)) {
            page = userDao.findAll(new SpringPageParamAdapter(pageParam));
        } else {
            page = userDao.findByLoginNameLikeOrUserNameLike('%' + keyword + '%', '%' + keyword + '%', new SpringPageParamAdapter(pageParam));
        }
        PageResult<User> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(userEntityToView).convert(this::fill);
    }

    public List<UserView> online() {
        List<UserView> onlineUsers = new ArrayList<>();
        List<WebSocketSession> webSocketSessions = webSocketFeign.get();
        for (WebSocketSession webSocketSession : webSocketSessions) {
            User user = userDao.findById(webSocketSession.getUserId()).orElseThrow(() ->
                    new HttpException(HttpStatus.NOT_FOUND, "Can't find User with id: " + webSocketSession.getUserId()));
            onlineUsers.add(userEntityToView.apply(user));
        }
        return onlineUsers;
    }
}
